package pl.edu.agh.jemo.evolution.specimen;

import java.util.Arrays;

import pl.edu.agh.jemo.evolution.common.JemoRandom;
import pl.edu.agh.jemo.evolution.genotype.Genotype;
import pl.edu.agh.jemo.evolution.objfunc.ObjectiveFunction;
import pl.edu.agh.jemo.evolution.objfunc.ObjectiveFunctionSet;
import pl.edu.agh.jemo.evolution.objfunc.impl.ExtremaFinderObjectiveFunction;

/**
 * Specimen is a type wrapping found solutions. Every specimen represents one point in genotype space.
 * It's position in problem domain space, is represented by genotype. Phenotype is a calculated value of objective function.
 * Specimen's fitness is overall rating of specimen and it's value is based on algorithm used for calculations.
 * 
 * @author Marcin Zbijowski
 *
 */
public class Specimen implements Comparable<Specimen>, Cloneable {

	/**
	 * Values of specimen's genotype. They are used as an arguments for objective functions.
	 */
	protected Genotype[] genotype;
	/**
	 * Values of specimen's phenotype. They are results of objective functions for genotype.
	 */
	protected Double[] phenotype;
	/**
	 * Fitness value. Number discribing quality of specimen. It is calculated by algorithm.
	 */
	protected Double fitness = 0.;

	/**
	 * Problem definition reference is stored for every specimen. It allows easy acces to domain and easier phenotype calculation.
	 */
	protected ObjectiveFunctionSet objectiveFunctions;

	/**
	 * Prepares new specimen instance to be used.
	 * Initializes genotype for all required dimensinos. Makes sure, new specimen lies inside boundaries.
	 * This method requires complete problem definition, to work properly.
	 * 
	 * @param objectiveFunctions Reference to problem definition for which specimen must be created.
	 * @param genotypeType Type of genotype to be used with specimen.
	 * @throws InstantiationException if creation of new genotype instance fails, for example because of lack of nullary constructor.
	 * @throws IllegalAccessException if there is no public access nullary constructor defined for genotype. 
	 */
	public void init(ObjectiveFunctionSet objectiveFunctions,
			Class<? extends Genotype> genotypeType)
	throws InstantiationException, IllegalAccessException {
		genotype = new Genotype[objectiveFunctions.getDimensions()];
		for (int i = 0; i < objectiveFunctions.getDimensions(); i++) {
			genotype[i] = genotypeType.newInstance();
			genotype[i].generateValue(objectiveFunctions.getBoundaries()[i]);
		}
		phenotype = new Double[objectiveFunctions.size()];
		this.objectiveFunctions = objectiveFunctions;
	}

	/**
	 * Returns currently set genotype value.
	 * 
	 * @return Genotype of specimen, as an array of Genotype instances.
	 */
	public Genotype[] getGenotype() {
		return genotype;
	}

	/**
	 * Sets genotype of specimen to given.
	 * New genotype should be unused object, not referenced by any other object.
	 * 
	 * @param genotype New genotype of specimen.
	 */
	public void setGenotype(Genotype[] genotype) {
		this.genotype = genotype;
	}

	/**
	 * Returns currently calculated phenotype, for given object.
	 * Does not recalculate phenotype if genotype has changed.
	 * 
	 * @return Array of phenotype values containing specimen phenotype.
	 */
	public Double[] getPhenotype() {
		return phenotype;
	}

	/**
	 * Returns current fitness value.
	 * Does not recalculate this value after any specimen change.
	 * 
	 * @return Value of current specimen's phenotype.
	 */
	public Double getFitness() {
		return fitness;
	}

	/**
	 * Sets current fitness, to new double value.
	 * 
	 * @param fitness New value of specimen firness parameter.
	 */
	public void setFitness(Double fitness) {
		this.fitness = fitness;
	}

	/**
	 * Calculates distance between object, and given specimen.
	 * Calculations are made based on phenotypes and euclidean distance.
	 * 
	 * @param specimen Other specimen, to which distance is calculated.
	 * @return Double value of distance between two specimen's phenotypes.
	 */
	public Double getEuclideanObjectiveSpaceDistance(Specimen specimen) {
		Double result = 0.;
		for (int i = 0; i < phenotype.length; i++) {
			result += Math.pow(phenotype[i] - specimen.getPhenotype()[i], 2);
		}
		return Math.sqrt(result);
	}

	/**
	 * Checks if specimen lies in problem's domain.
	 * 
	 * @return true if problem lies in problem domain on all dimensions of genotype, false otherwise.
	 */
	public boolean isInDomain() {
		return objectiveFunctions.isInDomain(this);
	}
	
	/**
	 * Modifies specimen genotype, to move it to a closest point in domain.
	 */
	public void moveToDomain() {
		for (int i = 0; i < genotype.length; i++) {
			if (genotype[i].asDouble() < objectiveFunctions.getBoundaries()[i][0]) {
				genotype[i].fromDouble(objectiveFunctions.getBoundaries()[i][0]);
			} else if (genotype[i].asDouble() > objectiveFunctions.getBoundaries()[i][1]) {
				genotype[i].fromDouble(objectiveFunctions.getBoundaries()[i][1]);
			} else if (genotype[i].asDouble().isNaN()) {
				if (JemoRandom.getRandom().nextBoolean()) {
					genotype[i].fromDouble(objectiveFunctions.getBoundaries()[i][1]);
				} else {
					genotype[i].fromDouble(objectiveFunctions.getBoundaries()[i][0]);
				}
			}
		}
	}
	
	/**
	 * Calculates distance between object, and given specimen.
	 * Calculations are made based on genotypes and euclidean distance.
	 * 
	 * @param specimen Other specimen, to which distance is calculated.
	 * @return Double value of distance between two specimen's genotypes.
	 */
	public Double getEuclideanGenotypeSpaceDistance(Specimen specimen) {
		Double result = 0.;
		for (int i = 0; i < genotype.length; i++) {
			result += Math.pow(genotype[i].asDouble()
					- specimen.getGenotype()[i].asDouble(), 2);
		}
		return Math.sqrt(result);
	}

	/**
	 * Compares two specimen from phenotype point of view.
	 * Tries to determine if any of specimen can be treated as stronger without use of any advanced comparison methods.
	 * 
	 * @param anotherSpecimen Reference to specimen, that is going to be compared with object.
	 * @return DOMINATES if given specimen is weaker on at least one dimension and isn't stronger on any, EQUALS if all phenotypes are equal, IS_DOMINATED if specimen is stronger on at least one dimension, and is never weaker. Otherwise returns INCOMPARABLE.
	 */
	public Domination dominationRelationWith(Specimen anotherSpecimen) {
		Domination result = Domination.IS_EQUAL;
		for (int i = phenotype.length - 1; i >= 0; i--) {
			// dla extrema findera sprawdzic czy dominacja >95%
			ObjectiveFunction checkIfExtrema = objectiveFunctions.get(i);
			if (checkIfExtrema instanceof ExtremaFinderObjectiveFunction) {
				Double dominationRatio = ((ExtremaFinderObjectiveFunction) checkIfExtrema)
						.getDominationRatio();
				if (phenotype[i] >= dominationRatio) {
					if (anotherSpecimen.phenotype[i] >= dominationRatio) {
						return Domination.INCOMPARABLE;
					} else {
						return Domination.DOMINATES;
					}
				} else if (anotherSpecimen.phenotype[i] >= dominationRatio) {
					return Domination.IS_DOMINATED;
				}
				// nie extrema finder
			}

			// pozostale
			if (phenotype[i] > anotherSpecimen.phenotype[i]) {
				if (result == Domination.IS_DOMINATED) {
					return Domination.INCOMPARABLE;
				} else {
					result = Domination.DOMINATES;
				}
			} else if (phenotype[i] < anotherSpecimen.phenotype[i]) {
				if (result == Domination.DOMINATES) {
					return Domination.INCOMPARABLE;
				} else {
					result = Domination.IS_DOMINATED;
				}
			}

		}
		return result;
	}
	
/*	//wersja zbija dominationrelation
	protected boolean extremumCondition(Specimen anotherSpecimen,
			ObjectiveFunction checkIfExtrema, int dim) {
		if (!(checkIfExtrema instanceof ExtremaFinderObjectiveFunction)) {
			return false;
		}
		ExtremaFinderObjectiveFunction extremaFinder = (ExtremaFinderObjectiveFunction) checkIfExtrema;

		if ((phenotype[dim] > extremaFinder.getDominationRatio())
				&& (phenotype[dim] > anotherSpecimen.phenotype[dim])) {
			return true;
		}
		return false;

	}

	public Domination dominationRelationWith(Specimen anotherSpecimen) {
		Domination result = Domination.IS_EQUAL;
		for (int i = 0; i < phenotype.length; i++) {
			if ((phenotype[i] > anotherSpecimen.phenotype[i])
					|| (extremumCondition(anotherSpecimen, objectiveFunctions
							.get(i), i))) {
				if (result == Domination.IS_DOMINATED) {
					return Domination.INCOMPARABLE;
				} else {
					result = Domination.DOMINATES;
				}
			} else if (phenotype[i] < anotherSpecimen.phenotype[i]) {
				if (result == Domination.DOMINATES) {
					return Domination.INCOMPARABLE;
				} else {
					result = Domination.IS_DOMINATED;
				}
			}
		}
		return result;
	}*/
	
	public Domination dominationRelationFirstCriteria(Specimen anotherSpecimen) {
		Domination result = Domination.IS_EQUAL;
		int i=0;
		//only first criteria
		//for (int i = 0; i < phenotype.length; i++) {
			if (phenotype[i] > anotherSpecimen.phenotype[i]) {
				if (result == Domination.IS_DOMINATED) {
					return Domination.INCOMPARABLE;
				} else {
					result = Domination.DOMINATES;
				}
			} else if (phenotype[i] < anotherSpecimen.phenotype[i]) {
				if (result == Domination.DOMINATES) {
					return Domination.INCOMPARABLE;
				} else {
					result = Domination.IS_DOMINATED;
				}
			}
		//}
		return result;
	}

	/**
	 * Generates new random values of genotype.
	 * Values generated are based on problem definition, it's boundaries and dimensions.
	 */
	public void generateGenotype() {
		for (int i = 0; i < genotype.length; i++)
			genotype[i].generateValue(objectiveFunctions.getBoundaries()[i]);
	}
	
	/**
	 * Calculates phenotype of specimen for each objective function.
	 */
	public void calculatePhenotype() {
		objectiveFunctions.sortSet();
		for (int i = 0; i < phenotype.length; i++) {
			phenotype[i] = objectiveFunctions.get(i).calculateValue(this);			
		}
	}

	/**
	 * Sets all values of new specimen, to be identical with object.
	 * This method is used by clone method. It has been divided from clone to allow it's usage from subclasses and prevent code duplication.
	 * It copies genotype, phenotype, fitness and problem reference.
	 * 
	 * @param newSpecimen Specimen, for which values are set to copies of object.
	 */
	protected void prepareCopy(Specimen newSpecimen) {

		// Copy values
		newSpecimen.fitness = fitness;
		newSpecimen.genotype = genotype.clone();
		for (int i = 0 ; i < genotype.length ; i++) {
			newSpecimen.genotype[i] = genotype[i].clone();
		}
		if (phenotype != null) {
			newSpecimen.phenotype = phenotype.clone();
		} else {
			newSpecimen.phenotype = null;
		}

		// Copy reference
		newSpecimen.objectiveFunctions = objectiveFunctions;
	}

	/**
	 * Creates new specimen identical to object, and of the same type as object.
	 * 
	 * @return New specimen, that is copy of current specimen.
	 */
	public Specimen clone() {
		Specimen result = null;
		try {
			result = new Specimen();
			prepareCopy(result);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		return result;
	}

	/**
	 * Compares to specimen using their phenotype.
	 * Result is based on domination relation, however both IS_EQUAL and INCOMPARABLE specimen are considered equal.
	 * 
	 * @param specimen Specimen reference that is to be compared with object.
	 * @return 0 if both specimen are equal, 1 if specimen is better than given, -1 if it's weaker.
	 */
	@Override
	public int compareTo(Specimen specimen) {
		Domination retVal = dominationRelationWith(specimen);
		if (retVal == Domination.INCOMPARABLE || retVal == Domination.IS_EQUAL)
			return 0;
		else if (retVal == Domination.DOMINATES)
			return 1;
		return -1;
	}
	
	/**
	 * Returns string format human readable specimen summary.
	 * Return srting contains info about genotype, phenotype and fitness of specimen.
	 * 
	 * @return String format human readable specimen summary
	 */
	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append("-=-=-=-=-=-=-=-=-=-=-=-=-=-=-\n");
		sb.append("BASE SPECIMEN:\n");
		sb.append("-=-=-=-=-=-=-=-=-=-=-=-=-=-=-\n");
		sb.append("GENOTYPE:\n");
		
		sb.append("[ ");
		for (int i = 0; i < genotype.length ; i++ ) {
			sb.append(genotype[i]);
			if (i < genotype.length - 1) {
				sb.append(" ; ");
			}
		}
		sb.append(" ]");
		
		sb.append("\n");
		sb.append("PHENOTYPE:\n");

		sb.append("[ ");
		if (phenotype != null) {
			for (int i = 0; i < phenotype.length ; i++ ) {
				sb.append(String.format("%.4f", phenotype[i]));
				if (i < phenotype.length - 1) {
					sb.append(" ; ");
				}
			}
		}

		sb.append(" ]");
		sb.append("\n");
		sb.append("FITNESS:\n");
		sb.append(String.format("%.4f", fitness));
		sb.append("\n");
		sb.append("-=-=-=-=-=-=-=-=-=-=-=-=-=-=-\n");
		return sb.toString();
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Specimen other = (Specimen) obj;
		if (fitness == null) {
			if (other.fitness != null)
				return false;
		} else if (!fitness.equals(other.fitness))
			return false;
		if (!Arrays.equals(genotype, other.genotype))
			return false;
		if (objectiveFunctions == null) {
			if (other.objectiveFunctions != null)
				return false;
		} else if (!objectiveFunctions.equals(other.objectiveFunctions))
			return false;
		if (!Arrays.equals(phenotype, other.phenotype))
			return false;
		return true;
	}
	
//	@Override
//	public int hashCode() {
//		final int prime = 31;
//		int result = 1;
//		result = prime * result + Arrays.hashCode(genotype);
//		/*result = prime * result + ((fitness == null) ? 0 : fitness.hashCode());
//		
//		result = prime
//				* result
//				+ ((objectiveFunctions == null) ? 0 : objectiveFunctions
//						.hashCode());
//		result = prime * result + Arrays.hashCode(phenotype);*/
//		return result;
//	}

	public double getEuclideanGenotypeSpaceDistance(Double[] center) {
		Double result = 0.;
		for (int i = 0; i < genotype.length; i++) {
			result += Math.pow(genotype[i].asDouble()
					- center[i], 2);
		}
		return Math.sqrt(result);
	}

	public ObjectiveFunctionSet getObjectiveFunctions() {
		return objectiveFunctions;
	}

	public void setObjectiveFunctions(ObjectiveFunctionSet objectiveFunctions) {
		this.objectiveFunctions = objectiveFunctions;
	}
}
